﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;

using Nest;
using ES.Client.Aggs;
using Elasticsearch.Net;

namespace ES.Client
{
    public class ElasticHub<T> where T : class
    {
        public ElasticHub()
        {
            var index = ConfigurationManager.AppSettings["esIndex"];
            if (string.IsNullOrWhiteSpace(index))
            {
                throw new Exception("请配置esIndex");
            }

            this.Index = index.ToLower();
            this.DocType = this.GetDocType().Name;
            var suffix = this.DocType.ToLower().Replace(".", "_");
            if (!this.Index.EndsWith(suffix))
            {
                this.Index = this.Index + "_" + suffix;
            }
        }

        public ElasticHub(string index)
        {
            if (string.IsNullOrWhiteSpace(index))
            {
                throw new Exception("index必须是非空字符串");
            }

            this.Index = index.ToLower();
            this.DocType = this.GetDocType().Name;
            var suffix = this.DocType.ToLower().Replace(".", "_");
            if (!this.Index.EndsWith(suffix))
            {
                this.Index = this.Index + "_" + suffix;
            }
        }

        /// <summary>
        /// 获取当前泛型对应的Elasticsearch类型
        /// </summary>
        /// <returns></returns>
        private ElasticsearchTypeAttribute GetDocType()
        {
            Type type = typeof(T);
            ElasticsearchTypeAttribute typeAttr = null;
            var attributes = type.GetCustomAttributes(true).ToList();
            foreach (var attr in attributes)
            {
                if (attr.GetType().FullName == typeof(Nest.ElasticsearchTypeAttribute).FullName)
                {
                    typeAttr = attr as ElasticsearchTypeAttribute;
                    break;
                }
            }

            if (typeAttr == null)
            {
                var error = string.Format("{0}缺少ElasticsearchType特性", type.FullName);
                throw new Exception(error);
            }

            return typeAttr;
        }

        public string Index
        {
            get;
            private set;
        }

        public string DocType
        {
            get;
            private set;
        }

        private string[] Nodes
        {
            get
            {
                var esUrl = ConfigurationManager.AppSettings["esUrl"];
                if (string.IsNullOrWhiteSpace(esUrl))
                {
                    throw new Exception("请配置esUrl");
                }

                var nodes = esUrl.Split(new string[1] { ";" }, StringSplitOptions.RemoveEmptyEntries);

                if (nodes == null || !nodes.Any())
                {
                    throw new Exception("请配置esUrl");
                }

                return nodes;
            }
        }

        static object _obj = new object();
        static ElasticClient _client = null;
        public ElasticClient Client
        {
            get
            {
                if (_client == null)
                {
                    lock (_obj)
                    {
                        if (_client == null)
                        {
                            var nodes = new List<Uri>();
                            foreach (var node in this.Nodes)
                            {
                                try
                                {
                                    nodes.Add(new Uri(node, UriKind.Absolute));
                                }
                                catch
                                {
                                    continue;
                                }
                            }

                            var pool = new StaticConnectionPool(nodes);
                            var settings = new ConnectionSettings(pool);
                            settings.DefaultFieldNameInferrer(f => f);
                            if (!string.IsNullOrWhiteSpace(this.Index))
                            {
                                settings.DefaultIndex(this.Index);
                            }

                            _client = new ElasticClient(settings);
                        }
                    }
                }

                return _client;
            }
        }

        /// <summary>
        /// 创建查询参数对象
        /// </summary>
        /// <returns></returns>
        public QueryParameter<T> NewParameter()
        {
            QueryParameter<T> parameter = new QueryParameter<T>(this.Index, this.DocType);
            return parameter;
        }

        /// <summary>
        /// 获取当前类型的Mapping
        /// </summary>
        /// <returns></returns>
        public TypeMapping GetMapping()
        {
            var key = this.Index + "_" + this.DocType + "_mapping";
            var _mapping = Cache.Get(key) as TypeMapping;
            if (_mapping == null)
            {
                lock (_obj)
                {
                    _mapping = Cache.Get(key) as TypeMapping;
                    if (_mapping == null)
                    {
                        var response = this.Client.GetMapping<T>();
                        if (response.IsValid && response.Mapping != null)
                        {
                            _mapping = response.Mapping;
                            Cache.Set(key, _mapping, 5 * 60);
                        }
                        else
                        {
                            throw new Exception(response.DebugInformation);
                        }
                    }
                }
            }

            return _mapping;
        }

        #region 判断字段类型
        /// <summary>
        /// 获取指定字段的Mapping结构
        /// </summary>
        /// <param name="field"></param>
        /// <returns></returns>
        public bool TryGetFieldMapping(string field, out IProperty property)
        {
            property = null;
            var key = this.Index + "_" + this.DocType + "_" + field;
            var _property = Cache.Get(key) as IProperty;
            if (_property == null)
            {
                var properties = this.GetMapping().Properties.ToList();
                _property = properties.FirstOrDefault(p => p.Key.Name == field).Value;
                if (_property != null)
                {
                    property = _property;
                    Cache.Set(key, _property, 30);
                    return true;
                }
            }

            return false;
        }

        public bool IsObjectField(string field)
        {
            IProperty property = null;
            if (this.TryGetFieldMapping(field, out property) && property.Type.Name == "object")
            {
                return true;
            }

            return false;
        }

        public bool IsNestedField(string field)
        {
            IProperty property = null;
            if (this.TryGetFieldMapping(field, out property) && property.Type.Name == "nested")
            {
                return true;
            }

            return false;
        }

        public bool IsNumberField(string field)
        {
            IProperty property = null;
            if (this.TryGetFieldMapping(field, out property) && property.Type.Name == "number")
            {
                return true;
            }

            return false;
        }

        public bool IsTextField(string field)
        {
            IProperty property = null;
            if (this.TryGetFieldMapping(field, out property) && property.Type.Name == "text")
            {
                return true;
            }

            return false;
        }

        public bool IsKeywordField(string field)
        {
            IProperty property = null;
            if (this.TryGetFieldMapping(field, out property) && property.Type.Name == "keyword")
            {
                return true;
            }

            return false;
        }

        public bool IsDateField(string field)
        {
            IProperty property = null;
            if (this.TryGetFieldMapping(field, out property) && property.Type.Name == "date")
            {
                return true;
            }

            return false;
        }

        public bool IsBooleanField(string field)
        {
            IProperty property = null;
            if (this.TryGetFieldMapping(field, out property) && property.Type.Name == "boolean")
            {
                return true;
            }

            return false;
        }

        public bool TryGetObjectField(string field, out Nest.ObjectProperty property)
        {
            property = null;
            var properties = this.GetMapping().Properties.ToList();
            var _property = properties.Find(p => p.Key.Name == field).Value;
            if (_property is Nest.ObjectProperty)
            {
                property = _property as Nest.ObjectProperty;
                return true;
            }

            return false;
        }

        public bool TryGetNestedField(string field, out Nest.NestedProperty property)
        {
            property = null;
            var properties = this.GetMapping().Properties.ToList();
            var _property = properties.Find(p => p.Key.Name == field).Value;
            if (_property is Nest.NestedProperty)
            {
                property = _property as Nest.NestedProperty;
                return true;
            }

            return false;
        }

        public bool TryGetNumberField(string field, out Nest.NumberProperty property)
        {
            property = null;
            var properties = this.GetMapping().Properties.ToList();
            var _property = properties.Find(p => p.Key.Name == field).Value;
            if (_property is Nest.NumberProperty)
            {
                property = _property as Nest.NumberProperty;
                return true;
            }

            return false;
        }

        public bool TryGetTextField(string field, out Nest.TextProperty property)
        {
            property = null;
            var properties = this.GetMapping().Properties.ToList();
            var _property = properties.Find(p => p.Key.Name == field).Value;
            if (_property is Nest.TextProperty)
            {
                property = _property as Nest.TextProperty;
                return true;
            }

            return false;
        }

        public bool TryGetKeywordField(string field, out Nest.KeywordProperty property)
        {
            property = null;
            var properties = this.GetMapping().Properties.ToList();
            var _property = properties.Find(p => p.Key.Name == field).Value;
            if (_property is Nest.KeywordProperty)
            {
                property = _property as Nest.KeywordProperty;
                return true;
            }

            return false;
        }

        public bool TryGetDateField(string field, out Nest.DateProperty property)
        {
            property = null;
            var properties = this.GetMapping().Properties.ToList();
            var _property = properties.Find(p => p.Key.Name == field).Value;
            if (_property is Nest.DateProperty)
            {
                property = _property as Nest.DateProperty;
                return true;
            }

            return false;
        }

        public bool TryGetBoolField(string field, out Nest.BooleanProperty property)
        {
            property = null;
            var properties = this.GetMapping().Properties.ToList();
            var _property = properties.Find(p => p.Key.Name == field).Value;
            if (_property is Nest.BooleanProperty)
            {
                property = _property as Nest.BooleanProperty;
                return true;
            }

            return false;
        }
        #endregion

        #region 搜索、分析、计数
        /// <summary>
        /// 根据指定分析器分析特定文本
        /// </summary>
        /// <param name="analyzer"></param>
        /// <param name="text"></param>
        /// <returns></returns>
        public IAnalyzeResponse AnalyzeByAnalyzer(string analyzer, string text)
        {
            var response = this.Client.Analyze(f => f.Index(this.Index).Analyzer(analyzer).Text(text));
            return response;
        }

        /// <summary>
        /// 根据指定字段使用的分析器分析特定文本
        /// </summary>
        /// <param name="field"></param>
        /// <param name="text"></param>
        /// <returns></returns>
        public List<AnalyzeToken> AnalyzeByField(string field, string text)
        {
            var tokens = new List<AnalyzeToken>();
            try
            {
                var response = this.Client.Analyze(f => f.Index(this.Index).Field(field).Text(text));
                if (response.IsValid)
                {
                    tokens = response.Tokens.ToList();
                }
            }
            catch
            {

            }

            return tokens;
        }

        public ISearchResponse<T> Search(QueryParameter<T> parameter)
        {
            var response = this.Client.Search<T>(search => search.
                             Query(query => query.Bool(b => b.
                               Must(parameter.Must.Build()).
                               MustNot(parameter.MustNot.Build()).
                               Should(parameter.Should.Build())
                               )).
                             From(parameter.From).
                             Size(parameter.Size).
                             Source(t => parameter.Source).
                             Highlight(h => parameter.Highlighter).
                             SearchType(parameter.SearchType).
                             TrackScores(parameter.TrackScores).
                             RequestCache(parameter.RequestCache).
                             Preference(parameter.Preference).
                             Sort(sort => parameter.BuildSorter()));

            return response;
        }

        public ICountResponse Count(QueryParameter<T> parameter)
        {
            var response = this.Client.Count<T>(search => search.Query(
                                   query => query.Bool(b => b.
                                       Must(parameter.Must.Build()).
                                       MustNot(parameter.MustNot.Build()).
                                       Should(parameter.Should.Build()))
                                   ));

            return response;
        }
        #endregion

        #region 聚合
        /// <summary>
        /// 聚合方法
        /// </summary>
        /// <param name="aggregationsSelector"></param>
        /// <returns></returns>
        public ISearchResponse<T> Aggregations(Func<AggregationContainerDescriptor<T>, IAggregationContainer> aggregationsSelector)
        {
            var response = this.Client.Search<T>(search => search.Aggregations(aggregationsSelector));
            return response;
        }

        /// <summary>
        /// DateTime类型字段聚合
        /// </summary>
        /// <param name="parameter"></param>
        /// <param name="field"></param>
        /// <param name="format"></param>
        /// <param name="interval"></param>
        /// <param name="order"></param>
        /// <returns></returns>
        public AggregationResponse<List<DateHistogramItem>> DateHistogram(QueryParameter<T> parameter, string field, string format, DateInterval interval, HistogramOrder order, string timezone = "+08:00")
        {
            var result = new AggregationResponse<List<DateHistogramItem>>();
            try
            {
                List<DateHistogramItem> histograms = new List<DateHistogramItem>();
                var response = this.Client.Search<T>(search => search.Aggregations(ar => ar.Filter("aggs", arf => arf.
                                                                                     Filter(filter => filter.
                                                                                            Bool(b => b.
                                                                                                 Must(parameter.Must.Build()).
                                                                                                 MustNot(parameter.MustNot.Build()).
                                                                                                 Should(parameter.Should.Build())
                                                                                                )
                                                                                           ).
                                                                                     Aggregations(ard => ard.
                                                                                                  DateHistogram("items", dh => dh.
                                                                                                                Field(field).
                                                                                                                Format(format).
                                                                                                                Interval(interval).
                                                                                                                TimeZone(timezone).
                                                                                                                Order(order)
                                                                                                               )
                                                                                                 ))
                                                                                  )
                                                    );

                if (!response.IsValid)
                {
                    string innerError = "";
                    if (response.ServerError != null && response.ServerError.Error != null &&
                        response.ServerError.Error.RootCause != null && response.ServerError.Error.RootCause.Any())
                    {
                        var _innerError = response.ServerError.Error.RootCause.FirstOrDefault();
                        if (_innerError != null)
                        {
                            innerError = _innerError.ToString();
                        }
                    }

                    innerError = string.IsNullOrEmpty(innerError) ? response.DebugInformation : innerError;
                    throw new Exception(innerError);
                }

                var aggs = response.Aggregations.First((arg) => arg.Key == "aggs").Value as Nest.SingleBucketAggregate;
                var bucketAgg = aggs.Aggregations.First(arg => arg.Key == "items").Value as Nest.BucketAggregate;
                var dateHistogramBuckets = bucketAgg.Items.Cast<Nest.DateHistogramBucket>().ToList();

                foreach (var item in dateHistogramBuckets)
                {
                    DateHistogramItem dateHistogramItem = new DateHistogramItem()
                    {
                        Key = (long)item.Key,
                        KeyAsString = item.KeyAsString,
                        DocCount = item.DocCount.HasValue ? item.DocCount.GetValueOrDefault() : 0
                    };

                    histograms.Add(dateHistogramItem);
                }

                result.IsValid = true;
                result.Documents = histograms;
            }
            catch (Exception ex)
            {
                result.IsValid = false;
                result.Error = ex.Message;
            }

            return result;
        }

        /// <summary>
        /// DateTime类型字段按天聚合
        /// </summary>
        /// <param name="parameter"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        public AggregationResponse<List<DateHistogramItem>> DayCount(QueryParameter<T> parameter, string field, string timezone = "+08:00")
        {
            var order = new HistogramOrder()
            {
                Key = "_key",
                Order = SortOrder.Descending
            };

            return this.DateHistogram(parameter, field, "yyyy-MM-dd", DateInterval.Day, order, timezone);
        }

        /// <summary>
        /// DateTime类型字段按月聚合
        /// </summary>
        /// <param name="parameter"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        public AggregationResponse<List<DateHistogramItem>> MonthCount(QueryParameter<T> parameter, string field, string timezone = "+08:00")
        {
            var order = new HistogramOrder()
            {
                Key = "_key",
                Order = SortOrder.Descending
            };

            return this.DateHistogram(parameter, field, "yyyy年MM月", DateInterval.Month, order, timezone);
        }

        /// <summary>
        /// DateTime类型字段按季度聚合
        /// </summary>
        /// <param name="parameter"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        public AggregationResponse<List<DateHistogramItem>> QuarterCount(QueryParameter<T> parameter, string field, string timezone = "+08:00")
        {
            var result = new AggregationResponse<List<DateHistogramItem>>();
            try
            {
                var order = new HistogramOrder()
                {
                    Key = "_key",
                    Order = SortOrder.Descending
                };

                var response = this.DateHistogram(parameter, field, "yyyy-MM", DateInterval.Quarter, order, timezone);
                if (!response.IsValid)
                {
                    throw new Exception(response.Error);
                }

                result.IsValid = true;
                var docs = response.Documents;
                foreach (var doc in docs)
                {
                    if (doc.KeyAsString.EndsWith("-01"))
                    {
                        doc.KeyAsString = doc.KeyAsString.Replace("-01", "一季度");
                        continue;
                    }

                    if (doc.KeyAsString.EndsWith("-04"))
                    {
                        doc.KeyAsString = doc.KeyAsString.Replace("-04", "二季度");
                        continue;
                    }

                    if (doc.KeyAsString.EndsWith("-07"))
                    {
                        doc.KeyAsString = doc.KeyAsString.Replace("-07", "三季度");
                        continue;
                    }

                    if (doc.KeyAsString.EndsWith("-10"))
                    {
                        doc.KeyAsString = doc.KeyAsString.Replace("-10", "四季度");
                        continue;
                    }
                }

                result.Documents = docs;
            }
            catch (Exception ex)
            {
                result.IsValid = false;
                result.Error = ex.Message;
            }

            return result;
        }

        /// <summary>
        /// DateTime类型字段按年聚合
        /// </summary>
        /// <param name="parameter"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        public AggregationResponse<List<DateHistogramItem>> YearCount(QueryParameter<T> parameter, string field, string timezone = "+08:00")
        {
            var order = new HistogramOrder()
            {
                Key = "_key",
                Order = SortOrder.Descending
            };

            return this.DateHistogram(parameter, field, "yyyy年", DateInterval.Year, order, timezone);
        }

        /// <summary>
        /// 根据指定字段分组聚合，计算每组数量
        /// </summary>
        /// <param name="parameter"></param>
        /// <param name="field"></param>
        /// <param name="missing"></param>
        /// <param name="size"></param>
        /// <param name="order"></param>
        /// <returns></returns>
        public AggregationResponse<List<TermsItem>> Terms(QueryParameter<T> parameter, string field, string missing, int size, TermsOrder order, int? minDocCount = null)
        {
            var result = new AggregationResponse<List<TermsItem>>();
            try
            {
                List<TermsItem> terms = new List<TermsItem>();

                var terms_request = new TermsAggregation("items")
                {
                    Field = new Field(field),
                    MinimumDocumentCount = minDocCount,
                    Missing = missing,
                    Size = size,
                    Order = new List<TermsOrder>
                    {
                        order
                    }
                };

                var response = this.Client.Search<T>(search => search.Aggregations(ar => ar.Filter("aggs", arf => arf.
                                                                                     Filter(filter => filter.
                                                                                            Bool(b => b.
                                                                                                 Must(parameter.Must.Build()).
                                                                                                 MustNot(parameter.MustNot.Build()).
                                                                                                 Should(parameter.Should.Build())
                                                                                                )
                                                                                           ).
                                                                                     Aggregations(art => art.
                                                                                                  Terms("items", dh => terms_request)
                                                                                                 ))
                                                                                  )
                                                    );

                if (!response.IsValid)
                {
                    string innerError = "";
                    if (response.ServerError != null && response.ServerError.Error != null &&
                        response.ServerError.Error.RootCause != null && response.ServerError.Error.RootCause.Any())
                    {
                        var _innerError = response.ServerError.Error.RootCause.FirstOrDefault();
                        if (_innerError != null)
                        {
                            innerError = _innerError.ToString();
                        }
                    }

                    innerError = string.IsNullOrEmpty(innerError) ? response.DebugInformation : innerError;
                    throw new Exception(innerError);
                }

                var aggs = response.Aggregations.First((arg) => arg.Key == "aggs").Value as Nest.SingleBucketAggregate;
                var bucketAgg = aggs.Aggregations.First(arg => arg.Key == "items").Value as Nest.BucketAggregate;
                var keyedBuckets = bucketAgg.Items.Cast<Nest.KeyedBucket<dynamic>>().ToList();

                foreach (var item in keyedBuckets)
                {
                    TermsItem termsItem = new TermsItem()
                    {
                        Key = item.Key,
                        KeyAsString = item.KeyAsString,
                        DocCount = item.DocCount.HasValue ? item.DocCount.GetValueOrDefault() : 0
                    };

                    terms.Add(termsItem);
                }

                result.IsValid = true;
                result.Documents = terms;
            }
            catch (Exception ex)
            {
                result.IsValid = false;
                result.Error = ex.Message;
            }

            return result;
        }
        #endregion
    }
}